package com.googlecode.ebasys.dao.hibernate;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.exception.NestableRuntimeException;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.StringUtils;

import com.googlecode.ebasys.entity.PersistentEntity;

/**
 * Misc utilities for use with Hibernate
 *
 * @author Tomas Varaneckas
 * @version $Id: HibernateUtils.java 58 2009-01-22 20:49:29Z tomas.varaneckas $
 */
public abstract class HibernateUtils {
    
    /**
     * Logger
     */
    private static final Log log = LogFactory.getLog(HibernateUtils.class);

    /**
     * Gets one big error message from Hibernate (or any other) exception.
     * All the nested reasons, no stack traces.
     * 
     * @param exception Huge nested Hibernate exception
     * @return Nice string with information about what really happened
     */
    public static String getErrorMessage(final Throwable exception) {
        final StringBuilder error = new StringBuilder();
        if (exception instanceof NestableRuntimeException) {
            error.append(StringUtils
                    .arrayToCommaDelimitedString((
                            (NestableRuntimeException) exception)
                            .getMessages()));
        } else {
            error.append(exception.getMessage());
            Throwable e = exception.getCause();
            while (e != null) {
                error.append(": ").append(e.getMessage());
                e = e.getCause();
            }
        }
        return error.toString();
    }
    
    /**
     * Creates a dynamic find by example query for PersistentEntity.
     * 
     * Fills the map with bindings. Dynamically chooses '=' or 'like' by 
     * analyzing example parameter contents.
     * 
     * @param entity Example entity
     * @param bindings Pre-instantiated empty map for bindings
     * @return SQL query for finding all objects similar to example entity
     */
    public static String createFindByExampleQuery(final PersistentEntity entity, 
            final Map<String, Object> bindings) {
        //build initial query - select e from Something e
        final StringBuilder queryBuilder = new StringBuilder("select e from ");
        queryBuilder.append(entity.getClass().getSimpleName()).append(" e");
        
        //parameter concatenator string. first param will start with 'where'
        String paramConcatString = " where ";
        
        //list of classes to get fields (for query params) from
        final List<Class<?>> classes = new ArrayList<Class<?>>();
        
        //add all entity classes + subclasses to the list. except the Object.
        classes.add(entity.getClass());
        Class<?> current = entity.getClass().getSuperclass();
        while (current != Object.class) {
            classes.add(current);
            current = current.getSuperclass();
        }
        
        //loop collected classes 
        for (final Class<?> klass : classes) {
            //loop class fields
            for (final Field field : klass.getDeclaredFields()) {
                //skip static and transient fields
                if (Modifier.isStatic(field.getModifiers()) 
                        || Modifier.isTransient(field.getModifiers())) {
                    continue;
                }
                try {
                    //get field value
                    Object value = ReflectionTestUtils.invokeGetterMethod(
                            entity, field.getName());
                    
                    //collections are excluded
                    if (value instanceof Iterable<?>) {
                        continue;
                    }

                    //replace * -> % for 'like' queries
                    if (value instanceof String) {
                        value = ((String) value).replaceAll("\\*", "%");
                    }
                    
                    //skip null valued fields
                    if (value == null) {
                        continue;
                    }
                    
                    //add ' where|and fieldName =|like :fieldName' to query
                    queryBuilder
                            .append(paramConcatString).append(field.getName());
                    
                    //add ' =|like ' to query
                    if (field.getType().equals(String.class) 
                            && ((String) value).contains("%")) {
                        queryBuilder.append(" like :");
                    } else {
                        queryBuilder.append(" = :");
                    }
                    queryBuilder.append(field.getName());

                    //set parameter concat string to ' and ' (for second+ params)
                    paramConcatString = " and ";
                    
                    //add fieldName -> value binding
                    bindings.put(field.getName(), value);
                } catch (final Exception e) {
                    log.warn("Failed checking field value: " + field.getName() 
                            + " on object: " + entity, e);
                }
            }
        }
        
        final String query = queryBuilder.toString();
        if (log.isTraceEnabled()) {
            log.trace("Created findByExample query from example: " + query);
            log.trace("Query bindings: " + bindings);
        }
        return query;
    }
}
